﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;

namespace Communication.Tcp
{
    public enum WorkType
    {
        Connection,
        Receive,
        Send
    };

    public class InvalidWorkTypeException : Exception
    {

    }

    /// <summary>
    /// IOCP-managed TCP Server.
    /// </summary>
    public class Server
    {
        #region Constants
        const int THREAD_KILL_TIMEOUT = 3000;
        public delegate void ServerEventHandler<T>(TcpClient client, T e);
        #endregion

        #region CTor
        public Server(int port, int max_connections, string bind_ip="", int workers=1, bool verbose=false) 
        {
            IPAddress ip;
            if (bind_ip == "")
                ip = Dns.GetHostAddresses(Dns.GetHostName())[0];
            else
                ip = IPAddress.Parse(bind_ip);

            this.m_clients = new List<TcpClient>(max_connections);

            this.m_gateway = new TcpListener(ip, port);
            this.m_verbose = verbose;
            this.m_startedEvent = false;

            this.m_maxConnections = max_connections;

            this.m_syncObject = new Object();

            this.m_iocpWorker = new Thread(new ThreadStart(this.iocpWork));

            this.m_factory = new TaskFactory();

            //TODO: fix that.
            //ThreadPool.SetMaxThreads(workers, workers);
        }
        #endregion

        #region Methods

        /// <summary>
        /// Starts the server.
        /// </summary>
        public void Start()
        {
            this.m_gateway.Start(this.m_maxConnections);
            this.m_startedEvent = true;

            Utilities.Logger.Instance.Log("Server is starting...");

            this.m_iocpWorker.Start();

            // TODO: Fix that.
            int workers, dummy;
            ThreadPool.GetMaxThreads(out workers, out dummy);

            Utilities.Logger.Instance.Log("Server started with {0} threads.", workers);
        }

        /// <summary>
        /// Stops the server.
        /// </summary>
        public void Stop()
        {

            Utilities.Logger.Instance.Log("Server is shutting down...");

            this.m_startedEvent = false;

            // Safely terminate the iocp thread
            this.m_iocpWorker.Join(THREAD_KILL_TIMEOUT);
            if (this.m_iocpWorker.IsAlive)
                this.m_iocpWorker.Abort();

            // TODO: Fix that.
            //ThreadPool.SetMaxThreads(0, 0);

            // Close the server gateway
            this.m_gateway.Stop();
            // Disconnect all of the clients
            foreach (TcpClient client in m_clients)
            {
                client.Close();
            }
            this.m_clients.Clear();

            Utilities.Logger.Instance.Log("Server shutted down.");
        }

        /// <summary>
        /// Sends data to a specified client
        /// </summary>
        /// <param name="client">The client to send the data</param>
        /// <param name="data">The data to send</param>
        public void SendToClient(TcpClient client, BufferedStream data)
        {
            Task task = m_factory.GenerateNewDataTask(WorkType.Send, client, data);
            ThreadPool.QueueUserWorkItem(new WaitCallback(this.work), task);
        }
        #endregion

        #region Properies
        public bool Running
        {
            get { return this.m_startedEvent; }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Gets the currently active sockets
        /// </summary>
        /// <returns>List of sockets</returns>
        private List<Socket> getClientSockets()
        {
            List<Socket> sockets = new List<Socket>();
            foreach (TcpClient client in this.m_clients)
            {
                sockets.Add(client.Client);
            }
            return sockets;
        }

        /// <summary>
        /// Get the client with the specified socket
        /// </summary>
        /// <param name="socket">Socket</param>
        /// <returns>Client</returns>
        private TcpClient getClientBySocket(Socket socket)
        {
            foreach (TcpClient client in this.m_clients)
            {
                if (client.Client == socket)
                    return client;
            }
            return null;
        }

        /// <summary>
        /// Format a byte-buffer
        /// </summary>
        /// <param name="buffer"></param>
        /// <returns></returns>
        private string formatBuffer(byte[] buffer)
        {
            string bytes = "";
            foreach (byte b in buffer)
            {
                bytes += string.Format("{0,2:X} ", b);
            }
            return bytes;
        }
        #endregion

        #region IOCP
        private void iocpWork()
        {

            while (m_startedEvent)
            {
                List<Socket> waiting_sockets = this.getClientSockets();
                waiting_sockets.Add(this.m_gateway.Server);

                try
                {
                    try
                    {
                        // Poll the connected clients
                        Socket.Select(waiting_sockets, null, null, -1);
                    }
                    catch (NullReferenceException)
                    {
                        continue;
                    }


                    foreach (Socket socket in waiting_sockets)
                    {
                        // Is that socket is our gateway?
                        if (socket == this.m_gateway.Server)
                        {
                            if (this.m_gateway.Pending())
                            {
                                // There is a connection!
                                try
                                {
                                    Task task = m_factory.GenerateNewTask(WorkType.Connection);
                                    ThreadPool.QueueUserWorkItem(new WaitCallback(this.work), task);
                                }
                                catch (TaskAlreadyExistsException)
                                {
                                }
                            }
                        }
                        else
                        {
                            TcpClient client = this.getClientBySocket(socket);
                            if (client == null)
                            {
                                // Client disconnected
                            }
                            else
                            {
                                try
                                {
                                    Task task = m_factory.GenerateNewTask(WorkType.Receive, client);
                                    ThreadPool.QueueUserWorkItem(new WaitCallback(this.work), task);
                                }
                                catch (TaskAlreadyExistsException)
                                {
                                }
                            }
                        }
                    }
                }
                catch (SocketException)
                {

                }
            }
        }

        /// <summary>
        /// Handles disconnection of a client
        /// </summary>
        /// <param name="client"></param>
        private void handleDisconnect(TcpClient client)
        {
            if (OnDisconnect != null)
                OnDisconnect(client, EventArgs.Empty);
            client.Close();
            lock (this.m_clients)
            {
                this.m_clients.Remove(client);
            }
        }

        /// <summary>
        /// Handles data recieve from a client
        /// </summary>
        /// <param name="client"></param>
        private void handleReceive(TcpClient client)
        {
            lock (client)
            {
                try
                {
                    byte[] byte_buffer = new byte[client.Available];
                    int bytes_read = client.Client.Receive(byte_buffer);
                    
                    if (this.m_verbose)
                    {
                        Utilities.Logger.Instance.Log("Recevied packet: {0}", formatBuffer(byte_buffer));
                    }

                    BufferedStream buffer = new BufferedStream(new MemoryStream(byte_buffer, 0, bytes_read)); 
                    if (OnReceive != null)
                        OnReceive(client, new RecvEventArgs(buffer));
                }
                catch (SocketException e)
                {
                    switch (e.SocketErrorCode)
                    {
                        case SocketError.ConnectionReset:
                            // client disconnected.
                            handleDisconnect(client);
                            break;

                        default:
                            if (OnError != null)
                                OnError(client, new ErrorEventArgs(e));
                            break;
                    }
                }
                catch (NullReferenceException)
                {
                }
            }
        }

        /// <summary>
        /// Handles new connection.
        /// </summary>
        private void handleConnection()
        {
            lock (this.m_gateway)
            {
                TcpClient client = this.m_gateway.AcceptTcpClient();
                lock (this.m_clients)
                {
                    this.m_clients.Add(client);
                }
                
                if (this.m_clients.Count > this.m_maxConnections)
                {
                    // Max connections exceeded.
                    handleDisconnect(client);
                }

                if (OnConnect != null)
                    OnConnect(client, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Handles data send
        /// </summary>
        /// <param name="client"></param>
        /// <param name="data"></param>
        private void handleSend(TcpClient client, BufferedStream data)
        {
            lock (client)
            {
                try
                {
                    byte[] data_byte = new byte[data.Length];
                    data.Read(data_byte, 0, (int)data.Length);
                    if (this.m_verbose)
                    {
                        Utilities.Logger.Instance.Log("Sending packet: {0}", formatBuffer(data_byte));
                    }
                    client.Client.Send(data_byte);
                    if (OnSend != null)
                        OnSend(client, EventArgs.Empty);
                }
                catch (SocketException e)
                {
                    switch (e.SocketErrorCode)
                    {
                        case SocketError.ConnectionReset:
                            handleDisconnect(client);
                            break;

                        default:
                            if (OnError != null)
                                OnError(client, new ErrorEventArgs(e));
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Handles received Task
        /// </summary>
        /// <param name="state_info"></param>
        private void work(Object state_info)
        {
            Task info = state_info as Task;

            switch (info.Type)
            {
                case WorkType.Connection:
                    this.handleConnection();
                    break;
                case WorkType.Receive:
                    this.handleReceive(info.Client);
                    break;
                case WorkType.Send:
                    DataTask data_info = info as DataTask;
                    this.handleSend(data_info.Client, data_info.Data);
                    break;
                default:
                    throw new InvalidWorkTypeException();
            }
            info.Done();

        }
        #endregion

        #region Events
        public event ServerEventHandler<ErrorEventArgs> OnError;
        public event ServerEventHandler<EventArgs> OnDisconnect;
        public event ServerEventHandler<EventArgs> OnConnect;
        public event ServerEventHandler<RecvEventArgs> OnReceive;
        public event ServerEventHandler<EventArgs> OnSend;
        #endregion

        #region Fields
        private object m_syncObject;
        private TcpListener m_gateway;
        private List<TcpClient> m_clients;
        private bool m_startedEvent;
        private bool m_verbose;
        private int m_maxConnections;
        private Thread m_iocpWorker;
        private TaskFactory m_factory;
        #endregion Fields
    }
}
